package hac

import (
	"GLibHac/fssystem"
	"GLibHac/ns"
	"GLibHac/nsfs"
	"GLibHac/tools"
	"fmt"
	"github.com/pkg/errors"
	"path/filepath"
	"strings"
)

var UnsupportedFileType = errors.New("unsupported file type")

func checkPartitionFileSystemStatus(keySet *nsfs.KeySet, pfs *nsfs.PartitionFileSystem) (hasMainNca, isExeFs bool, err error) {
	err = pfs.EnumerateEntries("/", ".*", func(entry *nsfs.DirectoryEntryEx) error {
		ext := strings.ToLower(filepath.Ext(entry.Name))
		if ext == ".nca" || ext == ".ncz" {
			file, err := pfs.OpenFile(entry.Path, nsfs.OpenFileModeRead)
			if err != nil {
				return err
			}
			nca, err := nsfs.NewNca(keySet, file)
			if err != nil {
				return err
			}
			if nca.Header.ContentType == nsfs.NcaContentTypeProgram && nca.SectionExists(nsfs.NcaSectionTypeData) {
				hasMainNca = true
				return nsfs.StopEnumerateErr
			} else if filepath.Ext(entry.Path) == ".main" {
				isExeFs = true
			}
		}
		return nil
	})
	return
}

func LoadApplication(keySet *nsfs.KeySet, applicationFile string) (gameInformation *GameInformation, err error) {
	ext := filepath.Ext(applicationFile)
	switch ext {
	case ".nsp", ".xci", ".nsz", ".xcz":
		{
			var storage *fssystem.FileStorage
			storage, err = fssystem.NewFileStorage(applicationFile)
			if err != nil {
				return nil, err
			}
			defer storage.Close()
			gameInformation = &GameInformation{}
			var pfs *nsfs.PartitionFileSystem
			var hasMainNca bool
			var isExeFs bool
			if ext == ".xci" || ext == ".xcz" {
				var xci *nsfs.Xci
				xci, err = nsfs.NewXci(keySet, storage)
				if err != nil {
					return nil, err
				}
				pfs, err = xci.OpenPartition(nsfs.XciPartitionTypeSecure)
				if err != nil {
					return nil, err
				}
			} else {
				pfs, err = nsfs.NewPartitionFileSystem(storage)
				if err != nil {
					return nil, err
				}
				hasMainNca, isExeFs, err = checkPartitionFileSystemStatus(keySet, pfs)
				if err != nil {
					return nil, err
				}
				if !(hasMainNca || isExeFs) {
					err = fillContentMetaInfo(keySet, pfs, gameInformation)
					if err != nil {
						return nil, err
					}
					return
				}
			}
			err = fillContentMetaInfo(keySet, pfs, gameInformation)
			if err != nil {
				return nil, err
			}
			if isExeFs {
				npdmFile, err := pfs.OpenFile("/main.npdm", nsfs.OpenFileModeRead)
				if err != nil {
					return nil, err
				}
				npdm, err := ns.NewNpdm(npdmFile.AsStorage())
				if err != nil {
					return nil, err
				}
				gameInformation.TitleName = npdm.TitleName
				gameInformation.TitleId = fmt.Sprintf("%x", npdm.Aci0.TitleId)
				return gameInformation, nil
			} else {
				controlFs, titleId, err := GetControlFsAndTitleId(keySet, pfs)
				if err != nil {
					return nil, err
				}
				gameInformation.TitleId = titleId
				controlHolder, err := readControlData(controlFs)
				if err != nil {
					return nil, err
				}
				err = fillGameInformation(controlHolder, gameInformation)
				if err != nil {
					return nil, err
				}
				iconData := make(map[ns.Language][]byte)
				err = controlFs.EnumerateEntries("/", "icon_.*\\.dat", func(entry *nsfs.DirectoryEntryEx) error {
					name := entry.Name
					language := ns.ConvertNameToLanguage(name[strings.Index(name, "_")+1 : strings.Index(name, ".")])
					icon, err := controlFs.OpenFile(entry.Path, nsfs.OpenFileModeRead)
					if err != nil {
						return err
					}
					all, err := icon.ReadAll()
					if err != nil {
						return err
					}
					iconData[language] = all
					return nil
				})
				if err != nil {
					return nil, err
				}
				for _, v := range gameInformation.TitleInfos {
					if data, ok := iconData[v.Language]; ok {
						v.Icon = data
					}
				}
			}
		}
	default:
		return nil, errors.WithMessagef(UnsupportedFileType, "bad file type \"%s\"", ext)
	}
	return
}

func fillGameInformation(holder fssystem.IStorage, gameInformation *GameInformation) error {
	applicationControlProperty, err := ns.NewApplicationControlProperty(holder)
	if err != nil {
		return err
	}
	for i := 0; i < len(applicationControlProperty.Title); i++ {
		applicationTitle := applicationControlProperty.Title[i]
		if applicationTitle.Name != "" {
			gameInformation.TitleInfos = append(gameInformation.TitleInfos, &TitleInfo{
				Name:      applicationTitle.Name,
				Language:  ns.Language(i),
				Publisher: applicationTitle.Publisher,
			})
		}
	}
	gameInformation.Version = tools.Utf8ZToString(applicationControlProperty.DisplayVersion)
	return nil
}

func readControlData(controlFs nsfs.IFileSystem) (fssystem.IStorage, error) {
	return controlFs.OpenFile("/control.nacp", nsfs.OpenFileModeRead)
}

func convertID(id uint64) string {
	idStr := fmt.Sprintf("%x", id)
	l := 16 - len(idStr)
	if l > 0 {
		return strings.ToUpper(strings.Repeat("0", l) + idStr)
	}
	return idStr
}

func GetControlFsAndTitleId(keySet *nsfs.KeySet, pfs *nsfs.PartitionFileSystem) (controlFs nsfs.IFileSystem, titleId string, err error) {
	_, _, controlNca, err := getGameData(keySet, pfs, 0)
	if err != nil {
		return nil, "", err
	}
	// Return the ControlFS
	controlNcaFs, err := controlNca.OpenFileSystemByType(nsfs.NcaSectionTypeData, nsfs.IntegrityCheckLevelNone)
	if err != nil {
		return nil, "", err
	}
	return controlNcaFs, convertID(controlNca.Header.TitleId), nil
}

func getGameData(keySet *nsfs.KeySet, pfs *nsfs.PartitionFileSystem, i int) (mainNca, patchNca, controlNca *nsfs.Nca, err error) {
	err = keySet.ImportTickets(pfs)
	if err != nil {
		return nil, nil, nil, err
	}
	err = pfs.EnumerateEntries("/", ".*", func(fileEntry *nsfs.DirectoryEntryEx) error {
		ext := strings.ToLower(filepath.Ext(fileEntry.Path))
		if ext == ".nsz" || ext == ".nca" {
			ncaFile, err := pfs.OpenFile(fileEntry.Path, nsfs.OpenFileModeRead)
			if err != nil {
				return err
			}
			nca, err := nsfs.NewNca(keySet, ncaFile.AsStorage())
			// 在非标准的文件中该校验可能会导致意外的错误
			//                int ncaProgramIndex = nca.header.getTitleId().and(new Uint16(0xF)).intValue();
			//                if (ncaProgramIndex != programIndex) {
			//                    return true;
			//                }
			if nca.Header.ContentType == nsfs.NcaContentTypeProgram {
				dataIndex, err := nsfs.GetSectionIndexFromType(nsfs.NcaSectionTypeData, nsfs.NcaContentTypeProgram)
				if err != nil {
					return err
				}
				if nca.SectionExists(nsfs.NcaSectionTypeData) {
					if header, err := nca.Header.GetFsHeader(dataIndex); err == nil && header.IsPatchSection() {
						patchNca = nca
					} else if err != nil {
						return err
					} else {
						mainNca = nca
					}
				} else {
					mainNca = nca
				}
			} else if nca.Header.ContentType == nsfs.NcaContentTypeControl {
				controlNca = nca
			}
		}
		return nil
	})
	if err != nil {
		return nil, nil, nil, err
	}
	return
}

func fillContentMetaInfo(keySet *nsfs.KeySet, pfs *nsfs.PartitionFileSystem, gameInformation *GameInformation) error {
	cnmt, err := loadGameCnmt(keySet, pfs)
	if err != nil {
		return err
	}
	gameInformation.TitleVersion = fmt.Sprintf("%d", cnmt.TitleVersion.Version)
	gameInformation.ContentMetaType = cnmt.Type
	gameInformation.MetaTitleId = convertID(cnmt.TitleId)
	gameInformation.TitleId = convertID(cnmt.ApplicationTitleId)
	minimumApplicationVersion := cnmt.MinimumApplicationVersion
	if minimumApplicationVersion != nil {
		gameInformation.MinimumApplicationVersion = minimumApplicationVersion.String()
	}
	minimumSystemVersion := cnmt.MinimumSystemVersion
	if minimumSystemVersion != nil {
		gameInformation.MinimumSystemVersion = minimumSystemVersion.String()
	}
	return nil
}

func loadGameCnmt(keySet *nsfs.KeySet, pfs *nsfs.PartitionFileSystem) (cnmtRef *ns.Cnmt, err error) {
	err = pfs.EnumerateEntries("/", ".*\\.cnmt.nca", func(entry *nsfs.DirectoryEntryEx) error {
		var ncaFile fssystem.IStorage
		ncaFile, err = pfs.OpenFile(entry.Path, nsfs.OpenFileModeRead)
		if err != nil {
			return err
		}
		var nca *nsfs.Nca
		nca, err = nsfs.NewNca(keySet, ncaFile.AsStorage())
		if nca.Header.ContentType == nsfs.NcaContentTypeMeta {
			var pfs0 nsfs.IFileSystem
			pfs0, err = nca.OpenFileSystem(0, nsfs.IntegrityCheckLevelNone)
			if err != nil {
				return err
			}
			err = pfs0.EnumerateEntries("/", ".*", func(mateEntry *nsfs.DirectoryEntryEx) error {
				var file fssystem.IStorage
				file, err = pfs0.OpenFile(mateEntry.Path, nsfs.OpenFileModeRead)
				if err != nil {
					return err
				}
				cnmtRef, err = ns.NewCnmt(file)
				if err != nil {
					return err
				}
				return nsfs.StopEnumerateErr
			})
			if err != nil {
				return err
			}
			if cnmtRef != nil && cnmtRef.Type == ns.ContentMetaTypeApplication {
				return nsfs.StopEnumerateErr
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	if cnmtRef != nil {
		return cnmtRef, nil
	}
	return nil, fmt.Errorf("cannot open meta nca file")
}
